import { ethers } from "ethers";
import { Interface } from '@ethersproject/abi'
import { abi as PAIR_ABI } from "~/config/abi/UniswapV2Pair.json";
import { abi as ERC20_ABI } from "~/config/abi/ERC20.json";
import { abi as FACTORY_ABI } from "~/config/abi/UniswapV2Factory.json";
import { abi as MULTICALL_ABI } from '~/config/abi/Multicall.json'


export class MetamaskProvider {
  static ethereum: any;
  static provider: ethers.providers.Web3Provider;
  static signer: ethers.Signer;

  static async unlock() {
    await MetamaskProvider.ethereum.sendAsync({ method: "eth_requestAccounts" }, () => {});
  }

  static init() {
    MetamaskProvider.ethereum = (window as any).ethereum;
    MetamaskProvider.provider = new ethers.providers.Web3Provider(MetamaskProvider.ethereum);
    MetamaskProvider.signer = MetamaskProvider.provider.getSigner()
  }
}

class Provider {
  // Ethers Provider
  providerNode: string;
  provider: ethers.providers.JsonRpcProvider;

  // Contracts
  // contractUniswapV2Factory: ethers.Contract;

  constructor(provider: string) {
    this.providerNode = provider
    this.provider = ethers.getDefaultProvider(this.providerNode) as ethers.providers.JsonRpcProvider
  }

  async getPairInfo(pairAddress: string) {
    const contractPair = useContract(pairAddress, PAIR_ABI, this.provider);

    const task_token0 = contractPair.token0();
    const task_token1 = contractPair.token1();
    
    const task_reserves = contractPair.getReserves();
    const res = await Promise.all([task_token0, task_token1, task_reserves]);

    const token0: string = res[0];
    const token1: string = res[1];
    const reserves = res[2];

    const contractERC20ofToken0 = useContract(token0, ERC20_ABI, this.provider)
    const contractERC20ofToken1 = useContract(token1, ERC20_ABI, this.provider)

    const task_symbol0 = contractERC20ofToken0.symbol();
    const task_symbol1 = contractERC20ofToken1.symbol();
    const res_tasks = await Promise.all([task_symbol0, task_symbol1]);
    const name0 = res_tasks[0];
    const name1 = res_tasks[1];

    const task_decimals0 = contractERC20ofToken0.decimals();
    const task_decimals1 = contractERC20ofToken1.decimals();
    const _res_tasks = await Promise.all([task_decimals0, task_decimals1]);
    
    const decimals0 = parseInt(_res_tasks[0].toString());
    const decimals1 = parseInt(_res_tasks[1].toString());

    const reserve0 = ethers.FixedNumber.from(reserves._reserve0)
      .divUnsafe(ethers.FixedNumber.from(ethers.BigNumber.from("1".padEnd(decimals0+1, '0'))))
      .toString();
    const reserve1 = ethers.FixedNumber.from(reserves._reserve1)
      .divUnsafe(ethers.FixedNumber.from(ethers.BigNumber.from("1".padEnd(decimals1+1, '0'))))
      .toString();

    return {
      id: pairAddress,
      decimals0,
      decimals1,
      token0: name0,
      token1: name1,
      token0Addr: token0,
      token1Addr: token1,
      reserve0,
      reserve1,
    };
  }

  async getTxInfo(pairAddress: string) {
    console.log("## Tx info");

    const contractPair = useContract(pairAddress, PAIR_ABI, this.provider);
    console.log("## 3");
    // const task_swap = contractPair.filters['Swap(address,uint256,uint256,uint256,uint256,address)']()
    // const task_burn = contractPair.filters.Burn()
    // const task_mint = contractPair.filters.Mint()
    // const res = await Promise.all([task_swap, task_burn, task_mint])
    // // return res
    // const swaps = res[0]
    // const burns = res[1]
    // const mints = res[2]
    // return {
    //   swaps,
    //   burns,
    //   mints,
    // }

    // Burn(address,uint256,uint256,address)
    // Mint(address,uint256,uint256)
    // Swap(address,uint256,uint256,uint256,uint256,address)
    const filter = {
      // address: pairAddress,

      // topics: [
      //   ethers.utils.id("Swap(address,uint256,uint256,uint256,uint256,address)"),

      // ],
      address: "registrar.firefly.eth",
      fromBlock: 3313425,
      toBlock: 3313430,
      topics: [ethers.utils.id("nameRegistered(bytes32,address,uint256)")],
    };
    const res = await this.provider.getLogs(filter);
    return res;
  }
}

const mapUrlToProvider = new Map<string, Provider>()
export function useProvider(providerUrl: string) {
  let provider = mapUrlToProvider.get(providerUrl)
  if (!provider) {
    provider = new Provider(providerUrl)
    mapUrlToProvider.set(providerUrl, provider)
  }
  return provider
}

// use provider.connection.url to divide JsonRpcProvider 
// (provider, address) => contract
const mapProviderAddressToContract = new Map<string, Map<string, ethers.Contract>>()
export function useContract(
  address: string,
  contractInterface: ethers.ContractInterface,
  provider: ethers.providers.JsonRpcProvider
) {
  const _addr = ethers.utils.getAddress(address)
  let mapAddressToContract = mapProviderAddressToContract.get(provider.connection.url)
  if (!mapAddressToContract) {
    mapAddressToContract = new Map<string, ethers.Contract>()
    mapProviderAddressToContract.set(provider.connection.url, mapAddressToContract)
  }
  let contract = mapAddressToContract.get(_addr)
  if (!contract) {
    contract = new ethers.Contract(_addr, contractInterface, provider)
    mapAddressToContract.set(_addr, contract)
  }
  return contract
}

export function useERC20(
  address: string,
  provider: ethers.providers.JsonRpcProvider
) {
  return useContract(address, ERC20_ABI, provider)
}

export function useFactory(
  address: string,
  provider: ethers.providers.JsonRpcProvider
) {
  return useContract(address, FACTORY_ABI, provider)
}

export function useMulticall(
  address: string,
  provider: ethers.providers.JsonRpcProvider
) {
  return useContract(address, MULTICALL_ABI, provider)
}

/**
 * MULTICALL
 */


export interface Call {
  address: string
  abi: any
  functionName: string,
  params: any[]
}

export async function batchCall(
  multicallAddress: string,
  provider: ethers.providers.JsonRpcProvider,
  calls: Call[]
) {
  
  const multicallContract = useMulticall(multicallAddress, provider)
  const interfaceList = calls.map(item => new Interface(item.abi))
  const fragmentList = calls.map((item, index) => interfaceList[index].getFunction(item.functionName))
  const paramsList = calls.map(item => item.params)

  const res = await multicallContract.aggregate(calls.map((item, index) => [
    item.address,
    interfaceList[index].encodeFunctionData(fragmentList[index], paramsList[index])
  ]))
  const rawList = res.returnData
  const decodedList = rawList.map((item: string, index: number) => interfaceList[index].decodeFunctionResult(fragmentList[index], item))

  return decodedList
}

/**
 * BATCH WRAPS
 */

export async function getPairInfoBatch(
  multicallAddress: string,
  provider: ethers.providers.JsonRpcProvider,
  pairAddresses: string[]
) {

  const len = pairAddresses.length

  const token0_calls = pairAddresses.map((pairAddress, index) => ({
    address: pairAddress,
    abi: PAIR_ABI,
    functionName: 'token0',
    params: [],
  }))
  const token1_calls = pairAddresses.map((pairAddress, index) => ({
    address: pairAddress,
    abi: PAIR_ABI,
    functionName: 'token1',
    params: [],
  }))
  const getReserves_calls = pairAddresses.map((pairAddress, index) => ({
    address: pairAddress,
    abi: PAIR_ABI,
    functionName: 'getReserves',
    params: [],
  }))

  let batchRes = await batchCall(
    multicallAddress,
    provider,
    token0_calls.concat(token1_calls).concat(getReserves_calls)
  )

  const token0List = batchRes.slice(0, len).map((item:any) => item[0])
  const token1List = batchRes.slice(len, len*2).map((item:any) => item[0])
  const reservesList = batchRes.slice(len*2, len*3)

  const symbol_0_calls = token0List.map((item: any, index: string) => ({
    address: item,
    abi: ERC20_ABI,
    functionName: 'symbol',
    params: [],
  }))

  const symbol_1_calls = token1List.map((item: any, index: string) => ({
    address: item,
    abi: ERC20_ABI,
    functionName: 'symbol',
    params: [],
  }))

  const decimals_0_calls = token0List.map((item: any, index: string) => ({
    address: item,
    abi: ERC20_ABI,
    functionName: 'decimals',
    params: [],
  }))

  const decimals_1_calls = token1List.map((item: any, index: string) => ({
    address: item,
    abi: ERC20_ABI,
    functionName: 'decimals',
    params: [],
  }))

  batchRes = await batchCall(
    multicallAddress,
    provider,
    symbol_0_calls.concat(symbol_1_calls).concat(decimals_0_calls).concat(decimals_1_calls)
  )

  const name0List = batchRes.slice(0, len).map((item:any) => item[0])
  const name1List = batchRes.slice(len, len*2).map((item:any) => item[0])
  const decimals0List = batchRes.slice(len*2, len*3).map((item:any) => parseInt(item[0].toString()))
  const decimals1List = batchRes.slice(len*3, len*4).map((item:any) => parseInt(item[0].toString()))
  

  const reserve0List = reservesList.map((item:any, index:number) => (
    ethers.FixedNumber.from(item._reserve0)
      .divUnsafe(
        ethers.FixedNumber.from(ethers.BigNumber.from("1".padEnd(decimals0List[index]+1, '0')))
      )
      .toString()
  ))

  const reserve1List = reservesList.map((item:any, index:number) => (
    ethers.FixedNumber.from(item._reserve1)
      .divUnsafe(
        ethers.FixedNumber.from(ethers.BigNumber.from("1".padEnd(decimals1List[index]+1, '0')))
      )
      .toString()
  ))

  return pairAddresses.map((item, index) => ({
    id: item,
    decimals0: decimals0List[index],
    decimals1: decimals1List[index],
    token0: name0List[index],
    token1: name1List[index],
    token0Addr: token0List[index],
    token1Addr: token1List[index],
    reserve0: reserve0List[index],
    reserve1: reserve1List[index],
  }))
}